/*
 * @(#)UsfServiceExecutor.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

import ognl.OgnlException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.nhncorp.usf.core.config.runtime.Action;
import com.nhncorp.usf.core.config.runtime.Assign;
import com.nhncorp.usf.core.config.runtime.BusinessObject;
import com.nhncorp.usf.core.config.runtime.BusinessObjectGroup;
import com.nhncorp.usf.core.config.runtime.Method;
import com.nhncorp.usf.core.config.runtime.ScriptInfo;
import com.nhncorp.usf.core.config.runtime.Service;
import com.nhncorp.usf.core.config.runtime.ServiceCondition;
import com.nhncorp.usf.core.config.runtime.ServiceConditionGroup;
import com.nhncorp.usf.core.exception.ExceptionHandler;
import com.nhncorp.usf.core.exception.InvocationException;
import com.nhncorp.usf.core.invoker.InvocationFuture;
import com.nhncorp.usf.core.invoker.InvocationFutureListener;
import com.nhncorp.usf.core.invoker.Invoker;
import com.nhncorp.usf.core.service.supplement.CacheEntryProvider;
import com.nhncorp.usf.core.service.supplement.CacheResultAcceptor;
import com.nhncorp.usf.core.service.supplement.MethodContextCleanup;
import com.nhncorp.usf.core.service.supplement.ParameterGenerator;
import com.nhncorp.usf.core.service.supplement.PostScriptExecutor;
import com.nhncorp.usf.core.service.supplement.PreScriptExecutor;
import com.nhncorp.usf.core.servlet.ServletContextHolder;
import com.nhncorp.usf.core.util.OgnlUtil;
import com.nhncorp.usf.core.util.ScriptUtil;

/**
 * default implementation of {@link ServiceExecutor}.
 *
 * @author Web Platform Development Team.
 */
public class UsfServiceExecutor extends LoggableServiceExecutor implements ServiceExecutor {
    private Log log = LogFactory.getLog(DefaultServiceExecutor.class);

    private ExceptionHandler exceptionHandler;

    /**
     * @param invoker Invoker
     * @see com.nhncorp.usf.core.service.ServiceExecutor#setInvoker(com.nhncorp.lucy.net.invoker.Invoker)
     */
    public void setInvoker(Invoker invoker) {
        // implement ServiceExecutor
    }

    /**
     * set ExceptionHandler to handler exceptions
     *
     * @param exceptionHandler ExceptionHandler
     */
    public void setExceptionHandler(ExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
    }

    /**
     * execute all business defined in {@link Action}.
     *
     * @param action        {@link Action}
     * @param dataStructure used parameters for executing methods of action
     * @return RESULT_STATUS
     * @throws Exception the Exception
     */
    public RESULT_STATUS execute(Action action, Map<String, Object> dataStructure) throws Exception {
        ServiceExecutionContext context = new ServiceExecutionContext(dataStructure);
        context.setAction(action);
        context.setExceptionHandler(exceptionHandler);

        try {
            List<Service> parallelServices = action.getBusinessObjectInfos();
            List<BusinessObject> parallelBOs = extractBO(parallelServices, context.getAttributes());
            executeParallelBO(parallelBOs, context);

            List<Service> sequentialServices = action.getControlBlockInfos();
            executeSequentialServices(sequentialServices, context);

        } catch (OgnlException ognlException) {
            log.error(ognlException);
            return RESULT_STATUS.failure;
        } catch (InvocationException invocationException) {
            log.error(invocationException);
            return RESULT_STATUS.failure;
        } catch (InterruptedException interruptedException) {
            log.error(interruptedException);
            return RESULT_STATUS.failure;
        }
        return RESULT_STATUS.success;
    }

	/**
     * {@code boServices} 를 병렬 수행한다.
     * 
     * @param boServices 실행할 BO 들
     * @param context 설정파일
	 * @throws Exception exception
     * */
    private void executeParallelBO(List<BusinessObject> boServices, ServiceExecutionContext context) throws Exception {
    	Map<MethodExecutor, MethodReturn> futuresMap = new HashMap<MethodExecutor, MethodReturn>();

    	for (int i = 0; i < boServices.size(); i++) {
    		BusinessObject bo = boServices.get(i);
            context.setBo(bo);

            for (Service service : bo.getServiceInfos()) {
            	if (ServiceType.method == service.getServiceType()) {
            		Method method = (Method)service;
                    context.setMethod(method);
                    context.setMethodType(ServiceExecutionContext.MethodType.parallel);

                    MethodExecutor methodExecutor = new MethodExecutor((ServiceExecutionContext)context.clone(), 3, 3);
                    setSupplements(methodExecutor, method);

                    if (methodExecutor.before()) {
                        try {
							futuresMap.put(methodExecutor, methodExecutor.execute(method));
						} catch (Exception e) {
							if (method.getIsIgnoreException()) {
								context.getAttributes().put(method.getReturnId(), null);
								log.debug("Exception Ignored at : " + context.getBo().getId() + "." + method.getName(), e);
							} else {
								throw e;
							}
						}
                    }

                    logMethodExecutionInfo(method, context);
            	} else {
            		// BO 내부에 IF 가 있을 경우 처리
            		List<BusinessObject> innerBOs = extractBO(service, context.getAttributes());
            		boServices.addAll(innerBOs);
            	}
            }
        }

        final CountDownLatch numberOfReturnedValues = new CountDownLatch(futuresMap.size());

        for (MethodReturn methodReturn : futuresMap.values()) {
            methodReturn.addFutureListener(new InvocationFutureListener() {

                public void invocationComplete(InvocationFuture future) throws Exception {
                    numberOfReturnedValues.countDown();
                }

            });
        }

        numberOfReturnedValues.await();

        for (Map.Entry<MethodExecutor, MethodReturn> entry : futuresMap.entrySet()) {
        	try {
        		entry.getValue().finish();
        		entry.getKey().after();
        	} catch (Exception e) {
        		Method method = entry.getValue().getMethod();

        		if (method.getIsIgnoreException()) {
					context.getAttributes().put(method.getReturnId(), null);
					log.debug("Exception Ignored at : " + (context.getBo() != null ? context.getBo().getId() : null) + "." + method.getName(), e);
				} else {
					throw e;
				}
        	}
        }

        futuresMap.clear();
        futuresMap = null;
    }

    /**
     * {@code services} 를 순차적으로 실행한다. BO 실행 순서 보장을 위해서 Service 를 하나씩 실행한다.
     * 
     * @param services 실행할 Service
     * @param context 설정파일
     * @throws Exception exception
     * */
    private void executeSequentialServices(List<Service> services,
			ServiceExecutionContext context) throws Exception {
		for (Service service : services) {
			List<BusinessObject> boServices = extractBO(service, context.getAttributes());
			executeSequentialBO(boServices, context);
		}
	}

    /**
     * {@code boServices} 를 순차 수행한다.
     * 
     * @param boServices 실행할 BO
     * @param context 설정파일
     * @throws Exception exception
     * */
    private void executeSequentialBO(List<BusinessObject> boServices, ServiceExecutionContext context) throws Exception {
    	for (BusinessObject boService : boServices) {
    		context.setBo(boService);

	    	for (Service service : boService.getServiceInfos()) {
	    		if (service.getServiceType() == ServiceType.method) {
	    			context.setMethodType(ServiceExecutionContext.MethodType.sequential);
	    			executeSequentialMethod((Method)service, context);
	    		} else {
	    			// BO 내에 if가 있는 경우 처리
	    			List<BusinessObject> innerBOs = extractBO(service, context.getAttributes());
	    			executeSequentialBO(innerBOs, context);
	    		}
	        }
    	}
	}

	/**
     * {@code services} 에서 최종 실행할 {@link BusinessObject} 목록을 반환한다.
     * 
     * @param services {@link Service} 목록
     * @param dataMap 입력 데이터
     * @return 최종 실행할 {@link BusinessObject} 목록
     * @throws Exception exception
     * */
    private List<BusinessObject> extractBO(List<Service> services, Map<String, Object> dataMap) throws Exception {
    	List<BusinessObject> boServices = new ArrayList<BusinessObject>();

    	for (Service service : services) {
    		List<BusinessObject> tempBOServices = extractBO(service, dataMap);

    		if (tempBOServices != null) {
    			boServices.addAll(tempBOServices);
    		}
    	}

    	return boServices;
	}

    /**
     * {@code service} 에서 최종 실행할 {@link BusinessObject} 목록을 반환한다.
     * 
     * @param service BO 를 포함하고 있는 Service (혹은 BO 가 없는 Service 도 있음) : bo, bo-group, if, elseif, else, if-group, condition-script(BO 없음, 단독 실행)
     * @param dataMap 입력 데이터
     * @return 최종 실행할 {@link BusinessObject} 목록
     * @throws Exception exception
     * */
	private List<BusinessObject> extractBO(Service service, Map<String, Object> dataMap) throws Exception {
		List<BusinessObject> boServices = null;

		switch (service.getServiceType()) {
	        case bo:					// bo
	        	logExecutionPath(service);
	        	boServices = new ArrayList<BusinessObject>();
	        	boServices.add((BusinessObject) service);
	            return boServices;
	        case bo_group :				// bo-group
	        	logExecutionPath(service);
	        	BusinessObjectGroup boGroup = (BusinessObjectGroup) service;
	        	boServices = getBOList(boGroup, dataMap);
	        	return boServices;
	        case condition_script :		// condition-script
	        	logExecutionPath(service);
	        	ScriptUtil.processControlScript(ServletContextHolder.getContextRealPath(), (ScriptInfo)service, dataMap);
	        	return Collections.emptyList();
	        case conditionBlock:		// if, elseif, else
	        	List<Service> serviceList = null;
	       		serviceList = getCheckedInfo((ServiceCondition)service, dataMap);
	        	boServices = new ArrayList<BusinessObject>();

	        	for (Service conditionService : serviceList) {
	        		boServices.addAll(extractBO(conditionService, dataMap));
	        	}

	        	return boServices;
	        case conditionGroupBlock:	// if-group
	        	logExecutionPath(service);
	       		serviceList = getCheckedInfo((ServiceConditionGroup)service, dataMap);
	        	boServices = new ArrayList<BusinessObject>();

	        	for (Service conditionService : serviceList) {
	        		boServices.addAll(extractBO(conditionService, dataMap));
	        	}

	        	return boServices;
	        case assign:
	        	logExecutionPath(service);
	        	OgnlUtil.evaluateAndGet(((Assign)service).getScript(), dataMap);
	        	return Collections.emptyList();
	        default:
	            throw new IllegalArgumentException("Unsupported ServiceType " + service);
    	}
	}

	/**
     * {@link BusinessObjectGroup} 의  {@link ScriptInfo} 를 통과한 BO 목록을 받는다.
     * 
     * @param boGroup {@link BusinessObjectGroup}
     * @param dataModel 입력 데이터
     * @return {@code List<BusinessObject>}
     * @throws Exception exception
     */
    @SuppressWarnings("unchecked")
	private List<BusinessObject> getBOList(BusinessObjectGroup boGroup, Map<String, Object> dataModel) throws Exception {
		List<BusinessObject> bos = new ArrayList<BusinessObject>();

		ArrayList boList = (ArrayList)ScriptUtil.execute(ServletContextHolder.getContextRealPath(),
		                                                 boGroup.getScriptInfo(), dataModel);
		List<Service> boInfos = boGroup.getServiceInfos();

		for (Service bo : boInfos) {
			if (ServiceType.bo != bo.getServiceType()) {
				continue;
			}

		    BusinessObject aBoInfo = (BusinessObject)bo;

		    if (boList.contains(aBoInfo.getId())) {
		        bos.add(aBoInfo);
		    }
		}

        return bos;
    }

    /**
     * Control 블럭 내부의 메소드 실행
     *
     * @param method  Method
     * @param context ServiceExecutionContext
     * @throws Exception the Exception
     */
    private void executeSequentialMethod(Method method, ServiceExecutionContext context) throws Exception {
        logMethodExecutionInfo(method, context);

    	context.setMethod(method);
        MethodExecutor methodExecutor = new MethodExecutor(context, 3, 3);
        setSupplements(methodExecutor, method);

        if (methodExecutor.before()) {
			try {
				methodExecutor.execute(method).finish();
			} catch (Exception e) {
				if (method.getIsIgnoreException()) {
					context.getAttributes().put(method.getReturnId(), null);
					log.debug("Exception Ignored at : " + context.getBo().getId() + "." + method.getName(), e);
				} else {
					throw e;
				}
			}
        }

        methodExecutor.after();
    }

	/**
     * 조건식 검사. true경우 등록된 {@link Service} 목록을 리턴
     *
     * @param conditionBlock   ServiceCondition
     * @param controlResultMap Map<String, Object>
     * @return List<Service>
     * @throws Exception the Exception
     */
    private List<Service> getCheckedInfo(ServiceCondition conditionBlock, Map<String, Object> controlResultMap) throws Exception {
    	ScriptInfo scriptInfo = conditionBlock.getScriptInfo();

    	if (scriptInfo != null) {
    		ScriptUtil.processControlScript(ServletContextHolder.getContextRealPath(), scriptInfo, controlResultMap);
    	}

        // if block
        if (conditionBlock.isCheck()) {
            Boolean isSatisfy = OgnlUtil.evaluateAndGet(conditionBlock.getCondition(), controlResultMap);

            if (!isSatisfy) {
                return Collections.emptyList();
            }
        }

        logExecutionPath(scriptInfo);
        logExecutionPath(conditionBlock);
        return conditionBlock.getServiceInfos();
    }

	/**
     * if-group 조건식을 검사해서 만족하는 {@link Service} 목록을 반환한다. <br/>
     * else 구분은 조건식이 true 이기 때문에 만족하는 것이 없으면 반드시 걸리게 되어 있다.
     * 
     * @param conditionGroup {@link ServiceConditionGroup}
     * @param controlResultMap 입력 데이터
     * @return 조건식을  만족하는 {@link Service} 목록
     * @throws Exception exception
     * */
	List<Service> getCheckedInfo(ServiceConditionGroup conditionGroup, Map<String, Object> controlResultMap) throws Exception {
    	List<Service> services = conditionGroup.getServiceInfos();

    	for (Service service : services) {
    		ServiceCondition serviceCondition = (ServiceCondition) service;
    		List<Service> innerServices = getCheckedInfo(serviceCondition, controlResultMap);

    		if (innerServices.size() > 0) {
    			return innerServices;
    		}
    	}

    	return Collections.emptyList();
	}

    /**
     * Sets the supplements.
     *
     * @param executor the executor
     * @param method   the method
     */
    private void setSupplements(MethodExecutor executor, Method method) {
        // before
        executor.addPreSupplement(new ParameterGenerator());

        // if (method.getPreScriptList().size() > 0)
        executor.addPreSupplement(new PreScriptExecutor());

        if (method.isUseCache()) {
            executor.addPreSupplement(new CacheEntryProvider());
            executor.addPostSupplement(new CacheResultAcceptor());
        }

        // after'
        // if (method.getPostScriptList().size() > 0)
        executor.addPostSupplement(new PostScriptExecutor());
        executor.addPostSupplement(new MethodContextCleanup());
    }
}
